exercise10 (Score: 25.0 / 25.0)

  1. Coding free-response (Score: 5.0 / 4.0)
  2. Comment
  3. Coding free-response (Score: 4.0 / 4.0)
  4. Written response (Score: 2.0 / 2.0)
  5. Comment
  6. Coding free-response (Score: 5.0 / 6.0)
  7. Comment
  8. Coding free-response (Score: 6.0 / 6.0)
  9. Written response (Score: 3.0 / 3.0)

Machine Learning - Aufgabenblatt 10

FHNW - FS2018


Bewertete Übung 3 (gemeinsam mit dem online Kurs)

Sie dürfen für die Lösung der Übung zusammenarbeiten, so lange sich ihre Zusammenarbeit auf algorithmische Fragestellungen beschränkt. Bei kopiertem Code oder Text (von Mitstudierenden oder dem Internet) werden alle Lösungen der beteiligten Parteien mit 0 Punkten bewertet. Dazu werden alle Lösungen manuell und automatisiert auf Kopien untersucht.


Einzureichen (online auf mlhub submitten) bis Dienstag, 5. Juni 2018, 24:00 Uhr


Ziel dieser Übung ist das Clustering von Immobilien mit Interpretation der entstandenen Cluster und die Dimensions-Reduktion von Features.

Wir verwenden dazu wiederum den vollen Datensatz /data/house_data.csv der Übung 1.

Vorgaben für die Abgabe :

  • Der Code muss ohne Änderungen lauffähig sein.
  • Sämtliche Plots müssen beschriftet sein (Achsen, Titel, kurze Beschreibung des Plots).
  • Eine Interpretation eines Plots beinhaltet eine Beschreibung sowie eine Diskussion der gezeigten Daten.
  • Sämtliche Algorithmen müssen selber implementiert werden. Die Verwendung von scikit-learn ist bloss zur Verifikation des Resultats gestattet.
In [1]:
#imports
import numpy as np
import pandas as pd
import math
import sys
import random

import matplotlib
from matplotlib import pyplot as plt
from matplotlib import cm
from matplotlib.colors import ListedColormap
from mpl_toolkits.mplot3d import Axes3D

%matplotlib inline

from sklearn.preprocessing import OneHotEncoder, StandardScaler, PolynomialFeatures
from scipy import sparse
from scipy.spatial.distance import cdist


import seaborn as sns

sns.set_style('whitegrid')

_ = np.seterr(all='raise') # to force errors in np to be thrown
_ = np.seterr(all='ignore') # to force errors in np to be thrown


def Abgabe_modus():
    '''Funktion um langsame plots auszuschalten während der Entwicklung'''
    return True
In [2]:
datapath = '/data/house_data.csv'
df = pd.read_csv(datapath)
print(df.columns)
Index(['id', 'date', 'price', 'bedrooms', 'bathrooms', 'sqft_living',
       'sqft_lot', 'floors', 'waterfront', 'view', 'condition', 'grade',
       'sqft_above', 'sqft_basement', 'yr_built', 'yr_renovated', 'zipcode',
       'lat', 'long', 'sqft_living15', 'sqft_lot15'],
      dtype='object')

Aufgabe 1 : Clustering mit k-Means (10 Punkte)

Clustern Sie die Immobilien indem Sie geeignete Features dafür suchen und allenfalls sinnvoll normalisieren. Erstellen Sie einen Elbow-Plot für $2 \leq k \leq 20$ und wählen Sie eine geeignete Cluster-Anzahl $k$. Führen Sie ein Clustering durch für die gewählte Cluster-Anzahl $k$, visualisieren Sie die entstandenen Cluster mit geeigneten Darstellungen und versuchen Sie, die Cluster zu charakterisieren (welche Eigenschaften haben die Gebäude im jeweiligen Cluster ?)

Tipp

Eine Möglichkeit, einen Cluster zu charakterisieren, besteht darin, dass Sie untersuchen, welche Features die kleinste Varianz haben (sich am ähnlichsten sind).

In [3]:
Student's answer Score: 5.0 / 4.0 (Top)
# Implementation Algorithm k-Means
class KMeans(object):
    def __init__(self, k=3, silent=False):
        ''' k-Means algorithms '''
        self.k = k
        self.silent = silent
        if (not self.silent):
            print('initializing k-means algorithm with {k} k'.format(k=k))
        
        
    def fit(self, X, use_kpp=True, number_of_initialisations=1000):
        best_score = sys.maxsize

        for n_init in range(0, number_of_initialisations):
            if (use_kpp):
                self.initialize_kpp(X)
            else:
                self.initialize_model(X)
                
            do_step = True
            while do_step:
                do_step = self.one_iteration(X)
                self.num_its_ += 1
            
            cf = self.cost_function(X)
            if (cf < best_score):                
                best_centroids = self.centroids_.copy()
                best_labels = self.labels_.copy()
                best_cost_ = self.cost_.copy()
                best_n_its = self.num_its_
                best_score = cf

        
        self.centroids_ = best_centroids.copy()
        self.labels_ = best_labels.copy()
        self.cost_ = best_cost_.copy()
        self.num_its_ = best_n_its
        if (not self.silent):
            print('finishing with cf', best_score)
            print('finishing with ITER COUNT', self.num_its_)
        return self
    
    def initialize_model(self, X):
        # initialize centroids
        self.centroids_ = X[np.random.randint(X.shape[0], size=self.k)]
        
        # assign labels first time
        self.labels_ = np.argmin(cdist(X, self.centroids_, metric="sqeuclidean"), axis=1)
        #print(self.labels_)
        #print(self.centroids_.shape)
        # we want to measure the cost function
        self.cost_ = []
        self.cost_.append(self.cost_function(X))
        self.num_its_ = 0
        
    def initialize_kpp(self, X):
        '''K-Means++ implementation'''
        startIdx= np.random.randint(X.shape[0], size=1)
        
        self.centroids_ = X[startIdx]
        
        copyX = X.copy()
        copyX = np.delete(copyX, [startIdx], axis=0)
        X_cdist = 0
        while (self.centroids_.shape[0] < self.k):
            D2 = np.amin(cdist(copyX, self.centroids_, metric='sqeuclidean'), axis=1)
            X2_sum = D2.sum()
            
            cum_probabilities = (D2/X2_sum)
            idx = np.random.choice(copyX.shape[0], 1, replace=False, p=cum_probabilities)[0]
            #r = random.random()
            #idx = np.where(cum_probabilities >=r)[0]
            self.centroids_ = np.vstack((self.centroids_, copyX[idx]))
            copyX = np.delete(copyX, [idx], axis=0)
            
            
        self.labels_ = np.argmin(cdist(X, self.centroids_, metric='sqeuclidean'), axis=1)
        #print(self.labels_)
        #print(self.centroids_.shape)
        # we want to measure the cost function
        self.cost_ = []
        self.cost_.append(self.cost_function(X))
        self.num_its_ = 0
        
    def one_iteration(self, X):
        ''' One KMeans iteration
        ATTENTION : kicks centroids out if no points are assigned
        returns False if centroids are unchanged, True otherwise
        '''
        old_centroids = self.centroids_.copy()
        # update centroids
        self.centroids_ = np.array([X[self.labels_ == k].mean(axis=0) for k in range(self.labels_.max()+1)])
        self.cost_.append(self.cost_function(X))
        # update labels
        self.labels_ = np.argmin(cdist(X, self.centroids_, metric="sqeuclidean"), axis=1)
        self.cost_.append(self.cost_function(X))
        return not np.array_equal(old_centroids, self.centroids_)
    
    def cost_function(self, X):
        '''The KMeans cost function.'''
        cost_k = []
        for ki in range(self.labels_.max()+1):
            Wk = cdist(X[self.labels_ == ki], np.atleast_2d(self.centroids_[ki]), metric="sqeuclidean").sum()
            cost_k.append(Wk)
        return np.array(cost_k).sum()
    
    def plot_kmeans(self, X, ax=None):
        KMeans.plot_kmeans_static(X=X, labels=self.labels_, centroids=self.centroids_, ax=ax)
    
    @staticmethod
    def plot_kmeans_static(X, labels, centroids=[], ax=None):
        colors = sns.color_palette("cubehelix", len(centroids))
        if ax is None:
            fig, ax = plt.subplots(figsize=(8,8))
        for idx, centroid in enumerate(centroids):
            ax.scatter(X[labels == idx, 0], X[labels == idx, 1], c=colors[idx])
            #ax.plot([centroid[0],],[centroid[1],], '+', color=colors[idx], mew=7, ms=25)
    
    def plot_kmeans_3d(self, X, ax=None):
        colors = sns.color_palette("cubehelix", len(self.centroids_))
        if (ax == None):
            fig = plt.figure()
            ax = Axes3D(fig)
        for idx, centroid in enumerate(self.centroids_):            
            ax.scatter(X[self.labels_ == idx, 0], X[self.labels_ == idx, 1],X[self.labels_ == idx,2], c=colors[idx])     
        
        ax.set_xlabel('component 1')
        ax.set_ylabel('component 2')
        ax.set_zlabel('component 3')
        return fig, ax
    
    def plot_feature_value_clusters(self, X, features):
        fig, ax = plt.subplots(figsize=(20,20))

        centroids = self.centroids_
        
        colors = sns.color_palette("Set1", len(centroids)*2)
        for idx, centroid in enumerate(centroids):
            means = X[self.labels_==idx].mean(axis=0)
            ax.plot(range(0,len(features)), means, color=colors[idx], label='mean_{c}'.format(c=idx))        
#             ax.plot(range(0,len(features)), centroid, color=colors[idx+len(centroids)], label='centroid_{c}'.format(c=idx))        

        ax.set_ylabel('Durchschnittlicher Wert')
        _ = plt.xticks(range(0,len(features)),features)
        _ = plt.xticks(rotation=90)
        plt.legend()
        return fig,ax
        
    def plot_feature_value_clusters_std(self, X, features):        
        X_display = StandardScaler().fit_transform(X)
        return self.plot_feature_value_clusters(X_display, features)
In [4]:
# test ob und wie k++ funktioniert:
def test_kpp():
    X = StandardScaler().fit_transform(np.array(df[['price', 'sqft_lot']]))
    cf_kpp = []
    cf_normal = []
    for i in range(0,20):
        km = KMeans(k=6, silent=True)
        km.fit(X, use_kpp=True, number_of_initialisations=5)
        cf_kpp.append(km.cost_function(X))

        km_normal = KMeans(k=6, silent=True)
        km_normal.fit(X, use_kpp=False, number_of_initialisations=10)
        cf_normal.append(km_normal.cost_function(X))
        
    plt.subplots(figsize=(8,5))
    plt.plot(cf_kpp, label="K-means++")
    plt.plot(cf_normal, label="K-means")
    plt.ylabel(r'J')
    plt.xlabel('Test #')
    plt.xticks(range(0,20))
    plt.title('Vergleich K-means++ (5 inits), K-means (10 inits)')
    _ = plt.legend()
    
test_kpp()
/opt/conda/lib/python3.6/site-packages/sklearn/utils/validation.py:475: DataConversionWarning: Data with input dtype int64 was converted to float64 by StandardScaler.
  warnings.warn(msg, DataConversionWarning)

Erkenntnis K-means++ / K-means

K-Means scheint mit 5 Initialisierungen mindestens so gut zu laufen wie 10 zufällige Initialisierungen. Ich entscheide mich deshalb im weiteren Verlaufe K-means++ zu verwenden

In [5]:
if (Abgabe_modus()):
    p = sns.pairplot(df)
    _= p.fig.suptitle('pairplot')

Beschreibung des Pairplot

Auf dem Pairplot sieht man alle Features auf der X-Achse gegenüber allen Features auf der Y-Achse dargestellt. Man sieht hier ein paar Features, welche stark miteinander korrelieren, sqft_above und sqft_living zeigen eine fast perfekte lineare Abhängigkeit. Wie bereits in der ersten bewerteten Übung gesehen, hangen sqft_living und log(price) ebenfalls stark zusammen. Wahrscheinlich hängen sqft_basement und sqft_above logarithmisch oder linear zusammen, das muss man herausfinden. sqft_above und bathrooms scheinen auch zu korrelieren.

Weiteres Vorgehen

Da k-means die least squared distance für die Optimierung verwendet, sind binäre Features (z.B. Waterfront) oder nicht kontinuierliche kategorische Features (z.B. zip) nicht sehr gut geeignet. (Der Centroid würde bei 4045.6 zuliegen kommen, was nicht eindeutig einer Postleizahl zugeordnet werden könnte.) Ich werde als nächstes von allen kontinuierlichen Features die Korrelation visualisieren und aufgrund dessen meine Features auswählen.

In [6]:
Student's answer Score: 4.0 / 4.0 (Top)
# Application
def km_elbow(X, lower_bound=2, upper_bound=20, title='elbow plot', use_kpp = True, n_inits = 10):
    cfs_X = []
    for k in range(lower_bound, upper_bound + 1):
        km = KMeans(k=k, silent=True)
        km.fit(X, use_kpp=use_kpp, number_of_initialisations=n_inits)
        cfs_X.append(km.cost_function(X))
        
    fig, ax = plt.subplots(figsize=(8,8))
    ax.plot(range(2,21),cfs_X)
    _ = ax.set_xticks(range(lower_bound,upper_bound + 1))
    _ = ax.set_title(title)
    _ = ax.set_xlabel('k')
    _ = ax.set_ylabel('J')
    return fig,ax

all_features = ['price', 'bedrooms', 'bathrooms', 'sqft_living',
       'sqft_lot', 'floors', 'waterfront', 'view', 'condition', 'grade',
       'sqft_above', 'sqft_basement', 'yr_built', 'yr_renovated', 'zipcode',
       'lat', 'long', 'sqft_living15', 'sqft_lot15']

cont_features = ['price', 'bedrooms', 'bathrooms', 'sqft_living','sqft_lot', 'floors', 'view', 'condition', 'grade',
       'sqft_above', 'sqft_basement', 'yr_built', 'yr_renovated', 'lat', 'long']
In [7]:
X = np.array(df[cont_features])
X_all = np.array(df[all_features])
xScaler = StandardScaler()
X_std = xScaler.fit_transform(X)     

#korrelation-matrix:
sigma = X_std.T.dot(X_std)/X_std.shape[0]

_ = plt.subplots(figsize=(10,10))
_ = plt.imshow(sigma, cmap="hot")
_ = plt.xticks(range(0,len(cont_features)),cont_features)
_ = plt.xticks(rotation=90)
_ = plt.yticks(range(0,len(cont_features)),cont_features)
_ = plt.title('Korrelations-Matrix der Features')

print('sum correlation')
for idx,s in enumerate(sigma.sum(axis=1)):
    print(cont_features[idx],':',s/len(cont_features))
    
sum correlation
price : 0.361434985934
bedrooms : 0.276484078851
bathrooms : 0.392353506741
sqft_living : 0.435100871949
sqft_lot : 0.130630789286
floors : 0.230290255482
view : 0.184897878389
condition : -0.00119301732468
grade : 0.388579133191
sqft_above : 0.390405895957
sqft_basement : 0.172451737479
yr_built : 0.19559326142
yr_renovated : 0.0769221014402
lat : 0.0866593930325
long : 0.159152435199

Erklärung Print

Im Print sieht man die Summe der korrelationen pro Feature. Hohe Werte bedeuten, dass das Feature mit vielen andern Features korreliert. Diese Darstellung soll mir nachher helfen Featuers zu eliminieren.

Interpretation der Korrelations-Matrix

Auf der Korrelationsmatrix dieht man auf der X und Y Achse jeweils die Features gegenüber gestellt. Die Werte sind, wie fest die Features miteinander korrelieren. Hohe Werte (hohe Korrelation) werden heller dargestellt, tiefere Werte (wenig Korrelation) rot / dunkler.

Die Diagonale ist (per definition) Weiss -> 100% Korrelation. Interessant ist, dass sqft_living und sqft_above sehr stark miteinander korrelieren. Ich werde daher nur eines der beiden Features verwenden. Aufgrund der Totalen Korrelation werde ich sqft_living nicht weiter verwenden.

Grade und sqft_living & sqft_above korrelieren auch sehr stark. da ich sqft_living bereits ausgeschlossen habe, werde ich als nächstes nochmals die totale Korrelation berechnen um zu entscheiden, welches der beiden Features (grade / sqft_above) ich ausschliesse

In [8]:
cont_features2 = ['price', 'bedrooms', 'bathrooms','sqft_lot', 'floors', 'view', 'condition', 'grade',
       'sqft_above', 'sqft_basement', 'yr_built', 'yr_renovated', 'lat', 'long']

X = np.array(df[cont_features2])
xScaler = StandardScaler()
X_std = xScaler.fit_transform(X)   
#korrelation-matrix:
sigma = X_std.T.dot(X_std)/X_std.shape[0]

print('sum correlation')
for idx,s in enumerate(sigma.sum(axis=1)):
    print(cont_features2[idx],':',s/len(cont_features2))
sum correlation
price : 0.337106409755
bedrooms : 0.255042177875
bathrooms : 0.366474094439
sqft_lot : 0.127616869855
floors : 0.221457467285
view : 0.177775499259
condition : 0.00291838049923
grade : 0.361855894386
sqft_above : 0.355677988619
sqft_basement : 0.153695220608
yr_built : 0.18684643945
yr_renovated : 0.0784620424807
lat : 0.089097245236
long : 0.153361659318

Entscheidung Grade / sqft_above

Wie vorher erklärt, sieht man hier nochmals die Summe der Korrelationen. Aufgrund dessen, dass grade die höhere Summe hat, werde ich dieses Feature ausschliessen.

Eigentlich korreliert price auch sehr stark mit den anderen Features. Ich werde daher das Modell 2 mal durchrechnen, einmal mit price und einmal ohne.

In [9]:
features = ['price', 'bedrooms', 'bathrooms','sqft_lot', 'floors', 'view', 'condition', 'sqft_above', 'sqft_basement', 'yr_built', 'yr_renovated', 'lat', 'long']
features_wo_price = ['bedrooms', 'bathrooms','sqft_lot', 'floors', 'view', 'condition', 'sqft_above', 'sqft_basement', 'yr_built', 'yr_renovated', 'lat', 'long']
X = np.array(df[features])
X_wo_price = np.array(df[features_wo_price])
X_std = StandardScaler().fit_transform(X)
X_std_wo_price = StandardScaler().fit_transform(X_wo_price)
In [10]:
_, _ = km_elbow(X, use_kpp=True, title='elbow plot')
_, _ = km_elbow(X_std, use_kpp=True, title='elbow plot std')
_, _ = km_elbow(X_wo_price, use_kpp=True, title='elbow plot ohne price')
_, _ = km_elbow(X_std_wo_price, use_kpp=True, title='elbow plot ohne price std')

Interpretation Elbow-Plots

Auf dem Elbowplot sieht man auf der X-Achse die anzahl Cluster, auf der Y-Achse den Cost_function wert (J) Wenn man die Features standardisiert, so scheint es keinen klaren Ellbogen mehr zu geben.

Ich werde mit folgenden k weiter arbeiten.:

  • Normal: 6
  • Std: 7
  • Ohne Preis: 5
  • Ohne Preis std: 8
In [11]:
def feature_value_clusters(X, k, title):
    km = KMeans(k=k)
    km.fit(X, use_kpp=True, number_of_initialisations=500)      
    fig, ax = km.plot_feature_value_clusters_std(X_all, all_features)
    fig.set_size_inches(10,10)
    ax.set_title(title)
    return km, fig, ax
In [12]:
km_normal,_,_ = feature_value_clusters(X, 6, 'Clustering Features')
initializing k-means algorithm with 6 k
finishing with cf 2.74207256527e+14
finishing with ITER COUNT 17
In [13]:
km_std,_,_ = feature_value_clusters(X_std, 7, 'Clustering Features std')
initializing k-means algorithm with 7 k
finishing with cf 150860.886119
finishing with ITER COUNT 44
In [14]:
km_wo_price,_,_ = feature_value_clusters(X_wo_price, 5, 'Clustering Features ohen Preis')
initializing k-means algorithm with 5 k
finishing with cf 2.57227246268e+12
finishing with ITER COUNT 7
In [15]:
km_std_wo_price,_,_ = feature_value_clusters(X_std_wo_price, 8, 'Clustering Features ohne Preis std')
initializing k-means algorithm with 8 k
finishing with cf 126875.079708
finishing with ITER COUNT 27
In [16]:
def print_cluster_size(km, title):
    unique, counts = np.unique(km.labels_, return_counts=True)
    totalcnt = km.labels_.shape[0]
    print(title,':',dict(zip(unique, counts)))
    print(title,'%:',dict(zip(unique, np.round(100* counts / totalcnt))))

print_cluster_size(km_normal, 'cluster size normal')
print_cluster_size(km_std, 'cluster size std')
print_cluster_size(km_wo_price, 'cluster size wo price')
print_cluster_size(km_std_wo_price, 'cluster size wo price std')
cluster size normal : {0: 9218, 1: 943, 2: 185, 3: 7640, 4: 11, 5: 3616}
cluster size normal %: {0: 43.0, 1: 4.0, 2: 1.0, 3: 35.0, 4: 0.0, 5: 17.0}
cluster size std : {0: 3239, 1: 4189, 2: 320, 3: 4918, 4: 1055, 5: 7023, 6: 869}
cluster size std %: {0: 15.0, 1: 19.0, 2: 1.0, 3: 23.0, 4: 5.0, 5: 32.0, 6: 4.0}
cluster size wo price : {0: 19963, 1: 11, 2: 1293, 3: 55, 4: 291}
cluster size wo price %: {0: 92.0, 1: 0.0, 2: 6.0, 3: 0.0, 4: 1.0}
cluster size wo price std : {0: 4580, 1: 4971, 2: 3366, 3: 3694, 4: 316, 5: 1243, 6: 2540, 7: 903}
cluster size wo price std %: {0: 21.0, 1: 23.0, 2: 16.0, 3: 17.0, 4: 1.0, 5: 6.0, 6: 12.0, 7: 4.0}

Interpretation Plots

Die Plots zeigen auf der X-Achse die verschiedenen Features. Auf der Y-Achse sind die durchschnittswerte (standardisiert zur besseren Übersichtlichkeit) pro Cluster dargestellt. Die Cluster werden mit verschiedenen Farben auseinander gehalten.

Clustering ohne zu standardisieren inkl. Preis

Man sieht hier, dass das Clustering ohne zu Standardisieren alle Werte recht gut trennt. Also alle hohen Werte sind in einem Cluster, alle tiefen in einem andern. Interessant ist, dass in einem Cluster extrem teure Häuser mit vielen Bädern und Schlafzimmern, grosser Fläche und guter Aussicht aber schlechtem Zustand sind. Die weiteren Cluster scheinen abstufungen zu sein, welche über alle Features gleich sind. Also hoher preis, viele Bäder, viel Platz etc., weniger hoher Preis, weniger Bäder, weniger Platz, etc.,.. Was auffällt, ist, dass etwa 43% in einem und 35 % in einem andern cluster landen. so sind also c.a. 3/4 aller Daten in zwei Cluster aufgeteilt. der 43% Cluster ist der mit den tiefsten Werten, also tiefer Preis, wenige Bäder etc. der 35%-Cluster ist der, der um 0 herum geht, also ziemlich in dem Mittelwert liegt. Der teure cluster macht nur knapp 1% aus.

Clustering standardisieren inkl. Preis

Hier scheint es etwas wilder zu und her zu gehen, was auffällt sind die Peask bei sqft_lot und yr_renovated und view. Der eine Cluster scheint vorallem Häuser mit viel Platz mit hoher long (also im Osten) zu beinhalten. ein anderer vorallem tiefe Preise und im süd-westen.

Hier ist die Cluster-verteilung ausgeglichener, der höchste anteil hat ein Clsuter mit 21%, was einem Cluster entspricht, der unterdurchschnittlich tiefe Preise, fast keine Bäder, wenigen Stockwerken, früh gebaut und geographisch im nord westen liegt entspricht. Interessant ist, dass es einen Cluster gibt mit extrem hohen Preisen und guter Aussicht, aber ein anderer mit teuren Preisen mit super Aussicht. Der springende Unterschied bei den beiden Clustern ist der Wohnraum welcher bei den extrem teuren Häusern vie lgrösser ist.

Clustering ohne standardisieren ohne Preis

Es zeigt sich hier ein ähnliches Bild wie beim Clustering ohne zu standardisieren inkl. Preis. Betrachtet man allerdings die Anteile der cluster etwas genauer, so fallen 92% in einen Cluster! Ich werde daher diesen Ansatz verwerfen und nicht weiter verfolgen. Das Clustering scheint sich ausschliesslich auf sqft_lot zu beziehen. Das war anzunehmen, da es sich hierbei um einen hohen Wert im Gegensatz zu den andern Features handelt.

Clustering standardisieren ohne Preis

Hier zeigt sich ein ähnliches Bild wie beim Clustering standardisieren inkl. Preis. Allerdings mit dem Unterschied, dass es wie beim Clustering ohne standardisieren ohne Preis einen Cluster mit extrem viel sqft_lot gibt. Dieser macht knapp 1% aller Häuser aus. Im gegensatz zu dem Clustering standardisieren inkl. Preis gibt es hier einen Cluster mit extrem hohen Preisen und super Aussicht und grossem Wohnraum.

In [17]:
#new dfs with cluster
df_normal = df.assign(cluster=km_normal.labels_)
df_std = df.assign(cluster=km_std.labels_)
df_std_wo_price = df.assign(cluster=km_std_wo_price.labels_)
df_full_std = pd.DataFrame(StandardScaler().fit_transform(X=np.array(df[all_features])),columns=all_features)
In [18]:
print('Varianz in den Cluster')

df_normal_var = df_full_std[features].assign(cluster=km_normal.labels_).groupby('cluster').var()
print('Totale Varianz: ', df_normal_var.sum().sum())
print('Durchscnittliche Varianz pro Cluster / Features: ', df_normal_var.sum().sum() / len(km_normal.centroids_) / len(features))
df_normal_var.style.background_gradient()

#df_normal.groupby('cluster').var()
Varianz in den Cluster
Totale Varianz:  114.914558131
Durchscnittliche Varianz pro Cluster / Features:  1.47326356579
Out[18]:
price bedrooms bathrooms sqft_lot floors view condition sqft_above sqft_basement yr_built yr_renovated lat long
cluster
0 0.0355876 0.804507 0.701249 0.38713 0.869787 0.228064 0.96345 0.363492 0.529382 0.803385 0.626034 1.16783 0.925504
1 0.414961 0.960034 1.03819 2.31494 0.815405 3.86927 1.16259 1.57051 2.22296 1.19892 2.69192 0.191385 0.637382
2 1.51577 1.11243 1.57658 0.994934 0.787793 4.88353 1.33508 1.97729 3.20737 1.47926 3.19353 0.121646 0.37413
3 0.041468 1.02729 0.691267 0.910589 1.06317 0.669928 0.968234 0.63429 0.905919 1.08798 0.849347 0.63678 1.13951
4 10.2328 0.420401 3.40263 0.0498362 0.420912 5.54234 0.386439 2.56771 10.1049 1.14871 5.40403 0.158116 0.073338
5 0.0955087 0.855166 0.805837 2.35103 0.815448 1.76965 1.09201 1.04729 1.34735 1.22704 1.60894 0.317659 0.990426
In [19]:
print('Varianz in den Cluster std')
df_std_var = df_full_std[features].assign(cluster=km_std.labels_).groupby('cluster').var()
print('Totale Varianz: ', df_std_var.sum().sum())
print('Durchscnittliche Varianz pro Cluster: ', df_std_var.sum().sum() / len(km_std.centroids_) / len(features))
df_std_var.style.background_gradient()
Varianz in den Cluster std
Totale Varianz:  84.6479456787
Durchscnittliche Varianz pro Cluster:  0.93019720526
Out[19]:
price bedrooms bathrooms sqft_lot floors view condition sqft_above sqft_basement yr_built yr_renovated lat long
cluster
0 0.909144 0.629097 0.612437 0.244728 0.194239 0.189806 0.368646 0.654811 0.623702 0.273765 0 0.56464 0.842217
1 0.400077 1.0347 0.399022 0.104868 0.302637 0.341945 1.30725 0.22663 0.748885 0.590262 0 0.81522 0.57202
2 0.826368 0.877666 1.26026 17.5092 0.797937 1.84707 0.854843 1.86412 1.30735 0.524384 0.602512 1.48773 1.29045
3 0.134242 0.496309 0.267133 0.0721925 0.707323 0.166097 0.221322 0.387151 0.142107 0.155221 0 1.15192 1.13424
4 4.94015 1.07811 1.38334 0.240689 1.07479 1.49065 1.20554 1.73457 2.2368 0.966967 0.834015 0.56192 0.708778
5 0.187939 0.532084 0.296512 0.0841915 0.192084 0.260933 1.12014 0.174888 0.241373 0.56536 0 1.05967 0.715054
6 1.35703 1.23357 1.1501 0.321683 0.829751 1.96036 0.613833 0.801916 1.21919 0.582087 0.00152388 0.786955 0.867453
In [20]:
print('Varianz in den Cluster ohne Preise std')
df_wo_price_std_var = df_full_std[features_wo_price].assign(cluster=km_std_wo_price.labels_).groupby('cluster').var()
print('Totale Varianz: ', df_wo_price_std_var.sum().sum())
print('Durchscnittliche Varianz pro Cluster: ', df_wo_price_std_var.sum().sum() / len(km_std_wo_price.centroids_) / len(features_wo_price))
df_wo_price_std_var.style.background_gradient()
Varianz in den Cluster ohne Preise std
Totale Varianz:  78.1544908976
Durchscnittliche Varianz pro Cluster:  0.814109280183
Out[20]:
bedrooms bathrooms sqft_lot floors view condition sqft_above sqft_basement yr_built yr_renovated lat long
cluster
0 0.60085 0.280453 0.0403445 0.243737 0.223742 1.04272 0.182177 0.296421 0.492413 0 0.465955 0.288837
1 0.601588 0.482882 0.183685 0.133493 0.16302 0.281018 0.78754 0.405451 0.191624 0 0.964322 0.753434
2 1.13972 0.469433 0.105012 0.36577 0.0585643 1.34787 0.291802 0.703396 0.623586 0 0.653619 0.514215
3 0.40319 0.395715 0.175117 0.205226 0.0884911 1.06937 0.203775 0.327621 0.342153 0 0.749838 1.01339
4 0.986148 1.51869 17.4632 0.806894 1.98324 0.822778 2.22129 1.61078 0.516448 0.609965 1.48821 1.33049
5 1.06844 1.2692 0.150204 1.014 1.06422 1.20446 1.40187 1.94033 0.867284 0.0597222 0.700052 0.740729
6 0.411161 0.410316 0.0236336 0.620126 0.129835 0.152177 0.33404 0.236131 0.165411 0 0.738192 0.526516
7 1.245 1.20872 0.314763 0.839882 2.49524 0.640957 0.935827 1.32102 0.590575 0.00150616 0.773383 0.848855

Interpretation der Varianzen

Varianz in den Cluster

4 von 5 Cluster zeigen eine geringe Varianz in Preis, Bäder und Kellerplatz. Es gibt einen Cluster, welcher eine sehr Hohe Varianz in den Preisen aufweist. Dies ist der Cluster mit den sehr hohen durchschnittspreisen. Die durchschnittliche Varianz (Summe der Varianz / anzahl Cluster / anzahl Features) liegt bei 1.4.

Varianz in den Cluster std

Hier fällt auf, dass vorallem sqft_lot sehr geringe Varianz hat und dieses Feature zum Clustern verwendet wurde. Die durchscnittliche Varianz pro Cluster liegt bei 0.93

Varianz in den Cluster ohne Preise std

5 von 8 Cluster haben generell eine niedrige Varianz. Zwei Cluster haben eine hohe Varianz in floor, view, condition, sqft_above, sqft_basement und year_built und ein Cluster steht heraus, da er durchgehend hohe Varianz aufweist. Die durchscnittliche Varianz pro Cluster: 0.81

Unterschiede

Ein grosser Unterschied zwischen der standardisierten Lösung und der nicht standardisierten ist, dass sqft_lot bei der nicht-standardisierten Lösung eine höhere Varianz aufweist als bei den standardisierten.

In [21]:
def plot_cluster_map(df, title):
    fig, ax = plt.subplots(figsize=(6,6))
    sc = ax.scatter(df['long'],df['lat'], c=np.array(df['cluster']), cmap=cm.Set1, alpha=0.5)
    _ = ax.set_xlabel('long')
    _ = ax.set_ylabel('lat')
    _ = ax.set_title(title)
    
    
plot_cluster_map(df_normal, 'Clusters auf der Karte')
plot_cluster_map(df_std, 'Clusters auf der Karte std')
plot_cluster_map(df_std_wo_price, 'Clusters auf der Karte ohne Preis std')

Interpretation der Plots

Karten Plots

Man sieht hier auf der X-Achse die long und auf der Y-Achse die lat. Colorcoded jeweils die Cluster. Generell ist zu sehen, dass obowohl lat und long zum Clustering hinzugezogen wurden, die meisten Cluster sehr grossräumig verteilt sind und nicht geographisch stark festhangen.

In [22]:
if (Abgabe_modus()):
    p = sns.pairplot(df_normal, hue='cluster', markers=['^','v','<','>','s','o'])
    _= p.fig.suptitle('pairplot normal')

    p_std = sns.pairplot(df_std, hue='cluster')
    _= p_std.fig.suptitle('pairplot std')

    p_std_wo_price = sns.pairplot(df_std_wo_price, hue='cluster')
    _= p_std_wo_price.fig.suptitle('pairplot ohne Preise std')

Interpretation der Plots

Auf den Pairplot sieht man nochmals die Cluster visualisiert. Auf der X-Achse sind alle Features gegenüber der Y-Achse dargestellt.

normal

Auf dem ersten Plot sieht man wie bereits vorher beobachtet, dass die trennung klar nach dem Preis erfolgt. Betrachtet man die Histogramme (Diagonale) fällt auf, dass die reihenfolge der Farben (von unten nach oben) immer eingehalten wird.

standardisiert

Auf dem Pairplot wird deutlich, dass es einen "minimalen"-cluster gibt, in dem die Häuser mit kleinen Werten drin sind. Das erkennt man an dem Farbigen Blob, der immer unten links in der Ecke des Pairplots liegt.

ohne Preis standardisiert

Auch hier sieht man ein Blob, welches unten Links meist unten links ist.


Die Pairplots dauern lange zum berechnen und geben nur wenig mehr Informationen her.

Ich entscheide mich die erste Cluster-Variante (ohne Standardisierung, inklusive Preis) zu erläutern:

In [23]:
for idx, centroid in enumerate(km_normal.centroids_):
    means = X[km_normal.labels_==idx].mean(axis=0)
    print('Durchschnittspreis Cluster',idx,means[0])
Durchschnittspreis Cluster 0 298667.058581
Durchschnittspreis Cluster 1 1439101.12195
Durchschnittspreis Cluster 2 2619758.77838
Durchschnittspreis Cluster 3 526441.518848
Durchschnittspreis Cluster 4 5531209.09091
Durchschnittspreis Cluster 5 828326.280144
Student's answer Score: 2.0 / 2.0 (Top)

Beschreibung der Cluster

Cluster

Beschreibung

Anteil Anzahl

0

Dieser Cluster enthält Häuser, welche extrem im Durchschnitt liegen. Durchschnittspreis: 524153

35% 7580

1

Hier sind Häuser enthalten, welche sehr teuer sind, viel Wohnraum, viele Bäder, viele Stockwerke haben. Kurz gesagt teure Häuser, welche sich fast niemand leisten kann. Durchscnittspreis: 2619758

1% 185

2

Dies sind ebenfalls Häuser welche teuer, viel Wohnraum und gute aussicht haben. allerdings sind diese im Schnitt etwas weniger teuer als die von Cluster 1. Allerdings sind die Häuser hier etwas moderner als die von Cluster 1. Durchscnittspreis: 1439101

4% 943

3

Dies sind die unterdurchschnittlichen Häuser. Tiefer Preis, wenige Schlaf- und Badezimmer sowie schlechte Aussicht. Land und Wohnraum ist ebenfalls kaum vorhanden. Durchschnittspreis: 298272

43% 9186

4

Das sind Häuser der besseren Mittelschicht. Der Pres und die Anzahl Schlafzimmer sind überdurchschnittlich aber nicht extrem. Die Häuser sind in einem guten Zustand und haben etwas Wohnraum. Durchschnittspreis: 824525

17% 3708

5

Extrem teure Häuser mit sehr viel Wohnraum, Schlafzimmer und Betten. Ausgezeichnete Aussicht und immer am Wasser. Allerdings ist der Zustand unterdurchschnittlich schlecht. Die Bewertung und der Platz oben und im keller sind auch extrem hoch. Die Häuser sind sehr neu (Baujahr) oder wurden kürzlich renoviert. Durchschnittspreis: 5531209

0% 11

Aufgabe 2 : Kombination Clustering - PCA (15 Punkte)

Unterziehen Sie das in Aufgabe 1 gewählte Modell einer Principal Component Analysis und clustern Sie die Immobilien diesmal auf den ersten 3 Principal Components. Erstellen Sie wiederum einen Elbow-Plot für $2 \leq k \leq 20$ und bestimmen Sie die geeignetste Cluster-Anzahl.
Führen Sie mit der gefundenen Cluster-Anzahl ein Clustering durch und charakterisieren Sie die entstandenen Cluster.

Diskutieren Sie den Unterschied der Cluster von Aufgabe 1 und Aufgabe 2.

Visualisieren Sie die ersten 3 Principal Components und suchen Sie nach Eigenschaften, welche das Clustering negativ beeinflussen könnten.

Genügt es, nur die ersten 3 Principal Components zu verwenden? Begründen Sie Ihre Antwort.

In [24]:
Student's answer Score: 5.0 / 6.0 (Top)
# Implementation Algorithm : PCA
def PCA(X, n):
    sigma = X.T.dot(X)/X.shape[0]
    U, S, V = np.linalg.svd(sigma)
    Ureduced = U[:, :n]
    Xreduced = X.dot(Ureduced)
    return Xreduced, U, S, V

def scatter_3d(X, fig=None, ax=None, color='blue', marker='o', alpha=0.5):
    if (fig == None):
        fig = plt.figure()
    if (ax == None):
        ax = Axes3D(fig)
    ax.scatter(X[:,0],X[:,1],X[:,2], c = color, marker=marker,alpha=alpha)
    ax.set_xlabel('component 1')
    ax.set_ylabel('component 2')
    ax.set_zlabel('component 3')
    return fig, ax
    

Xstd = StandardScaler().fit_transform(X)
Xreduced, U, S, V = PCA(Xstd,3)
_ = km_elbow(X=Xreduced)
In [25]:
fig, ax =scatter_3d(Xreduced)
_= ax.set_title('3D Scatter Plot PCA')

Interpretation der Plots

Elbow Plot

Auf dem Elbow-Plot sieht man wieder in der X-Achse die Anzahl Cluster gegeünber auf der Y-Achse den Wert der Cost-function. Die Kurve sieht sehr ähnlich aus wie bei der Aufgabe 1 als ich standardisiert habe. Ich entscheide mich hier 7 Clusters zu machen, auch wenn danach das Vergleichen schwieriger wird.

3D Scatter Plot

Auf dem ersten Plot sieht man die PCA visualisiert. auf der X-Achse Komponente1, Y-Achse Komponente 2 und auf der Z-Achse Komponente 3. Leider sind keine eindeutigen Gruppen zu erkennen, es scheint sich eher alles um 1 Zentrum zu bewegen.

In [26]:
percentVariance = S / S.sum()

fig, ax = plt.subplots()
ax.plot(percentVariance.cumsum())
ax.set_ylabel('Fraction of Explained Variance')
ax.set_xlabel('First N Components')
_ = ax.set_title('Explained fraction variance')
_ = ax.axvline(2, color='r', linewidth=2, label='3')
_ = ax.set_xticks(range(0,len(percentVariance)))
_ = ax.set_xticklabels(range(1, len(percentVariance) + 1))
print('explained variance for first 3 PCA: ', percentVariance.cumsum()[2])
explained variance for first 3 PCA:  0.521617592406

Beschreibung des Plots

Man sieht hier, dass 52 % der Varianz durch die ersten 3 PCA abgedeckt werden können.

In [27]:
Student's answer Score: 6.0 / 6.0 (Top)
# Application
km_pca, fig, ax = feature_value_clusters(X=Xreduced, k=7, title='Clustering PCA')
# km_pca = KMeans(k=7)
# km_pca.fit(Xreduced, use_kpp=True, number_of_initialisations = 500)
initializing k-means algorithm with 7 k
finishing with cf 42931.2225194
finishing with ITER COUNT 28
In [28]:
print_cluster_size(km_pca, 'Clustersizes PCA')
for idx, centroid in enumerate(km_pca.centroids_):
    means = X[km_pca.labels_==idx].mean(axis=0)
    print('Durchschnittspreis Cluster',idx,means[0])
Clustersizes PCA : {0: 4753, 1: 3667, 2: 3614, 3: 4073, 4: 721, 5: 2783, 6: 2002}
Clustersizes PCA %: {0: 22.0, 1: 17.0, 2: 17.0, 3: 19.0, 4: 3.0, 5: 13.0, 6: 9.0}
Durchschnittspreis Cluster 0 398185.313697
Durchschnittspreis Cluster 1 450725.467139
Durchschnittspreis Cluster 2 651350.274211
Durchschnittspreis Cluster 3 317400.627547
Durchschnittspreis Cluster 4 1702662.4147
Durchschnittspreis Cluster 5 507867.550485
Durchschnittspreis Cluster 6 918967.175824

Interpretation des Plots

Man sieht hier in der X-Achse alle Features und in der Y-Achse die werte Standardisiert dazu. Colorcoded sind die Cluster dargestellt. Es scheint so, als wären wieder ein Cluster vorhanden, der teure Häuser in schlechtem Zustand beinhaltet. Diesmal haben diese allerdings nicht eine so tolle Aussicht, dafür mehr land. Die Cluster werden weiter unten genauer diskutiert.

In [29]:
headers = ['Principal Component 1','Principal Component 2','Principal Component 3']
df_pca = pd.DataFrame(Xreduced, columns=headers)
df_pca_a2 = df_pca.assign(cluster=km_pca.labels_)
df_pca_a1 = df_pca.assign(cluster=km_normal.labels_)

p = sns.pairplot(df_pca_a2, hue='cluster', vars=headers)
p.fig.suptitle('Pairplot A2')
fig, ax = km_pca.plot_kmeans_3d(Xreduced)
ax.set_title('Clusters A2')
plot_cluster_map(df.assign(cluster=km_pca.labels_), 'Clusters auf der Karte')
In [30]:
print('Varianz in den Cluster')
df_pca_a2.groupby('cluster').var().style.background_gradient()
Varianz in den Cluster
Out[30]:
Principal Component 1 Principal Component 2 Principal Component 3
cluster
0 0.506945 0.480228 0.351683
1 0.428039 0.267839 0.713447
2 0.694045 0.677514 0.854184
3 0.572045 0.49497 0.87809
4 4.27159 2.20172 1.72595
5 0.571837 0.437397 0.425648
6 0.908146 0.721907 1.14106

Interpretation der Plots

Pairplot

Auf dem Pairplot sieht man in der X sowie in der Y-Achse die Features gegenübergestellt. Colorcoded sind die gebildeten Cluster zu erkennen.

3D Scatter Plot

Wiederum sieht man hier auf der X-Achse dne PCA 1, auf der Y-Achse den PCA 2 und auf der Z-Achse den PCA 3. Colorcoded sind die Cluster dargestellt.

Pairplot & 3D Scatter Plot

Beide Plots zeigen dieselben Daten aus unterschiedlichen perspektiven. Wie bereits angenommen kann man aus diesen Plots nicht viel herauslesen ausser, dass das Clustering funktionierte. Es wurden ähnlich grosse (flächenmässig gesehen) Cluster gebildet.

Karte

Auf der Karte sieht man in der X-Achse die long und in der Y-Achse die lat dargestellt. Colorcoded sind die Cluster zu erkennen. Man sieht hier, dass es durchaus Cluster gibt, welche in einer Region vorherrschend sind.

Varianz

In der Varianz-Tabelle sieht man welche Component für welchen Cluster ausschlaggebend sind. Man Sieht hier, dass sich nur ein Cluster für PC 3 interessiert. die anderen sind abängig von PC1 und 2.

Beschreibung der Cluster

Cluster

Beschreibung

Anteil Anzahl

0

Hier sind Häuser, welche etwas über dem Durchschnittspreis liegen, jedoch wenige Stockwerke haben geclustert. Der Zustand der Häuser ist ausgezeichnet und sie haben alle einen grossen Keller. Die Bauwerke sind eher alt. Durchschnittspreis: 651206

17% 3619

1

Unterdurchschnittlich günstige Häuser mit sehr vielen Stockwerken. der Zustand ist unterdurchschnittlich schlecht und es gibt nur einen kleinen Keller. Es sind meist neuere Bauwerke. Durchscnittspreis: 507867

13% 2783

2

Sehr günstige Häuser mit etwas Umschwung und in einem super guten Zustand. Es sind vorallem kleine Häuser mit wenigen Stockwerken, welche im Süden stehen. Durchscnittspreis: 317581

19% 4069

3

Das sind sehr teure Häuser, mit viel Platz, vielen Bädern und vielen Schlafzimmern. Allerdings ist hier der Zustand unterdruchschnittlich schlecht.Renoviert wurden diese Häuser schon länger nicht mehr, allerdings sind sie erst kürzlich gebaut worden. Die Häuser stehen vorallem im Nord-Osten Durchschnittspreis: 918967

9% 2002

4

Das sind die extrem teuren Häuser, viel Platz zum wohnen, extrem gute Aussicht, wahrscheinlich am Wasser mit einem grossen Keller und Esterich. Zudem wurden die Häuser erst kürzlich renoviert. Durchschnittspreis: 1702662

3% 721

5

Günstigere Häuser mit vielen Betten und Bädern. Ebenfalls viele Stockwere aber in einem schlechten Zustand. ein Keller ist quasi keiner vorhanden, obwohl es meist neubauten sind. die Häuser stehen im Süd-osten. Durchschnittspreis: 450725

17% 3367

6

günstige Häuser, welche sehr wenige Bäder und Schlafzimmer haben. Zudem ist hier wenig Platz zum leben und auch der Aussenbereich ist klein. Die aussicht ist bescheiden. Allerdings ist der Zustand des Hauses durchschnittlich. Die Häuser stehen im Nord-westen. Durchschnittspreis: 397805

22% 4752
In [31]:
p = sns.pairplot(df_pca_a1, hue='cluster', vars=headers)
p.fig.suptitle('Pairplot A1')
fig, ax = km_normal.plot_kmeans_3d(Xreduced)
_ = ax.set_title('Clusters A1')

Interpretation der Plots

Pairplot

Auf dem Pairplot sieht man in der X-Achse die Features gegenüber den Features auf der Y-Achse dargestellt. Colorcoded sind die Clsuter zu erkennnen. Man sieht hier, dass die Cluster aus Aufgabe 1 nicht örtlich (im Koordinaten System betractet) getrennt werden können, es gibt viele Überlappungen.

3D Scatter Plot

Wiederum sieht man hier auf der X-Achse dne PCA 1, auf der Y-Achse den PCA 2 und auf der Z-Achse den PCA 3. Colorcoded sind die Cluster aus Aufgabe 1 dargestellt.

Student's answer Score: 3.0 / 3.0 (Top)

Unterschied der Cluster von Aufgabe 1 und Aufgabe 2.

In Aufgabe 2 sind die Cluster von der Anzahl her ausgeglichener. Es gibt hier 5 Cluster, welche 90% der Daten abdecken und 2 welche die restlichen 10% ausmachen. Bei Aufgabe waren es 3, die 95% ausmachten und 3 welche die restlichen 5% waren. Bei Aufgabe 1 sind die Features mit grossen Werten definitiv eher ein Cluster-Kriterium als die Features mit tieferen Werten. Durch das Standardisieren in A2 wurde das etwas ausgebessert. Betrachtet man jedoch Aufgabe 2 mit der standardisierten Lösung aus A1 so sieht man gewisse Ähnlichkeiten. So gibt es in beiden Fällen einen Cluster mit überdurchschnittlichen Gebäuden. Einen Cluster mit teuren Häusern, welche in einem schlechten Zustand sind und auch ein "armer Cluster", in welchem alle Häuser die günstig & klein sind.

Eigenschaften, welche das Clustering negativ beeinflussen könnten

Um PCA anzuwenden msus man standardisieren. Das verzerrt jedoch das Ursprungsbild und kann so zu schlechteren / weniger aussagekräftigeren Cluster führen. Weiter dreht sich wie bereits festgestellt alles um ein Zentrum, es sind keine eindeutig unterscheidbaren Cluster zu erkennen. Da PCA in die Richtung der grössten Varianz geht, sind, streut der Datensatz natürlich in diese Richtung am meisten.

Genügt es, nur die ersten 3 Principal Components zu verwenden?

Wenn ich mir die beiden Resultate anschaue, so sehe ich Ähnlichkeiten, jedoch zu wenige um sagen zu können, dass das Resultat gleich ausfällt. Im Vergleich zu der standardisierten Variante ist es jedoch eine gute Alternative. Hier fallen die Cluster bei gleichem k ähnlich aus. In diesem Datensatz herrscht zudem generell eine hohe Korrelation. Das hilft sicherlich um mit PCA zu klassifizieren, da dadurch mit einem Feature viele abgedeckt sind.